package com.kamistoat.meimeimall.meimeimallgateway.filter;

import com.kamistoat.meimeimall.common.constant.AuthConstant;
import com.kamistoat.meimeimall.common.utils.JwtUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequest.Builder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.Collections;

/**
 * 网关全局拦截器。由于其优先级别设置为了-200，因此将在整个过滤器链中首个被执行。
 * 该拦截器只负责检验请求头中 Authorization 字段的有效性。
 * <p></p>
 * 如果没有携带 Authorization，直接放行。
 * 如果携带了 Authorization，则解析 Authorization 根据解析出的Token去缓存中检验有效性。
 * 如果有效，则将<b>解析后的 Authorization 放到请求头中，进行转发. </b>
 * <p></p>
 * 请求转发到对应服务模块时，直接根据请求头中解析好的 Authorization 提取用户令牌至ThreadLocal中。
 */
@Component
public class AuthFilter implements GlobalFilter, Ordered {
    private static final Logger logger = LoggerFactory.getLogger(AuthFilter.class);

    @Autowired
    StringRedisTemplate stringRedisTemplate;

    /**
     * GlobalFilter 只需要实现一个filter().
     * 验证 前端是否携带 Authorization，以及这个 Authorization 是否可以成功解析，并且和缓存中保存的后端登录令牌相对应。
     * <p></p>
     * 一旦通过后，则将网关的响应式请求，转换为普通的阻塞式请求。然后进行请求转发。
     * <p></p>
     *
     * @param exchange 拦截器拦截的内容
     * @param chain    Spring整个过滤器链抽象
     * @return 响应式编程结果 Mono<Void> 用于表示是否成功。
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 获取响应式Request。注意阻塞式Request是ServletHttpRequest
        ServerHttpRequest serverHttpRequest = exchange.getRequest();
        // 获取响应式Request的Builder
        Builder reqBuilder = serverHttpRequest.mutate();
        // 获取当前请求的目标url
        String targetUrl = serverHttpRequest.getURI().getPath();

        // 获取响应式请求中携带的 Authorization 字段即 jwtString
        String jwtString = getJwtStringServer(serverHttpRequest);
        // 如果没有携带 Authorization ，则直接放行
        if (StringUtils.isEmpty(jwtString)) {
            // logger.info("前端未携带Jwt令牌");
        } else {
            // 从JwtString中解析出三个构造用的三个字段
            String userId = JwtUtils.getUserIdFromJwtString(jwtString);
            String userToken = JwtUtils.getUserTokenFromJwtString(jwtString);
            String userName = JwtUtils.getUserNameFromJwtString(jwtString);

            if (StringUtils.isEmpty(userId) || StringUtils.isEmpty(userToken) || StringUtils.isEmpty(userName)) {
                logger.info("前端携带的Jwt令牌已过期或无法使用指定秘钥正确解析Jwt令牌，删除Jwt令牌");
            } else {
                // 根据Jwt解析的字段去缓存中查询。查询使用LUA脚本，在查询的同时延长过期时间
                String luaScript = "if redis.call('exists', KEYS[1]) then \n" +
                        "redis.call('expire', KEYS[1], ARGV[1]); \n" +
                        "return 1; \n" +
                        "else \n" +
                        "return 0; \n" +
                        "end;";
                Long execute = stringRedisTemplate.execute(
                        new DefaultRedisScript<Long>(luaScript, Long.class),
                        Collections.singletonList(getTokenCacheKey(userId, userToken)),
                        String.valueOf(AuthConstant.LOGIN_USER_REDIS_EXPIRE)
                );

                if (execute == null || execute.equals(0L)) {
                    logger.info("登录状态已经过期，或您的核心权限发生变更。请重新登录以获取最新权限");
                } else {
                    /**
                     * 响应式请求转阻塞式请求
                     */
                    // 将从 Authorization 即 JwtString 中解析出来的各种信息，直接以k-v对添加到请求的header中。
                    // 之后的模块直接从请求头获取这几个Header来去缓存中转存用户登录令牌
                    addHeader(reqBuilder, AuthConstant.JWT_USER_TOKEN, userToken);
                    addHeader(reqBuilder, AuthConstant.JWT_USER_ID, userId);
                    addHeader(reqBuilder, AuthConstant.JWT_USER_NAME, userName);

                    // 将携带上了一系列请求头数据的 ServerHttpRequest传递到过滤链的下一个
                    // chain.filter(传入ServerWebExchange)
                    // exchange.mutate() -- 获取当前 ServerWebExchange 的构造器(构造器将携带已经存在的参数构建)
                    // exchange.mutate().request() -- 使用exchange的构造器来为exchange添加ServerHttpRequest
                    // reqBuilder.build() -- 使用ServerHttpRequest的构造器来构建一个ServerHttpRequest
                    // 最后再使用 exchange的构造器.build() 把exchange重新构造出来，这样重新构造的exchange不仅携带已经存在的参数，也包含了更新了Header的ServerHttpRequest
                    return chain.filter(exchange.mutate().request(reqBuilder.build()).build());
                }
            }
        }

        // 否则不做任何处理放行，则请求头中不含有解析后的 Authorization
        return chain.filter(exchange.mutate().request(reqBuilder.build()).build());
    }

    /**
     * 该过滤器在过滤器链中的排序。
     *
     * @return
     */
    @Override
    public int getOrder() {
        return -200;
    }

    /**
     * 向 响应式请求 的请求头中添加k-v对
     *
     * @param reqBuilder 响应式请求 ServerHttpRequest 的构造器类
     * @param name       k
     * @param value      v
     */
    private void addHeader(Builder reqBuilder, String name, Object value) {
        if (!StringUtils.isEmpty(value)) {
            String valueStr = value.toString();
            String valueEncode = urlEncode(valueStr);
            // 使用 builder 向 ServerHttpRequest 请求头中添加新的k-v
            // 由于这里是使用builder，所以操作和直接操作 HttpRequest并不相同。
            // 顺便一提，ServerHttpRequest 和 ServletHttpRequest在构造出来后的操作方法基本相同
            reqBuilder.header(name, valueEncode);
        }
    }

    /**
     * 从 响应式请求 的请求头中移除 k-v对
     *
     * @param reqBuilder 响应式请求 ServerHttpRequest 的构造器类
     * @param name       k
     */
    private void removeHeader(Builder reqBuilder, String name) {
        if (!StringUtils.isEmpty(name)) {
            //  使用 builder 从 ServerHttpRequest 请求头中删除 k-v
            reqBuilder.headers(httpHeaders -> httpHeaders.remove(name));
        }
    }

    /**
     * 从 ServerHttpRequest中提取简易用户令牌。
     * 该方法和 SecurityUtils中的同名方法基本相同，只是那一个是对 非响应式的ServletHttpRequest 进行处理。
     * <p>
     * 至于为什么能从 serverHttpRequest 的header中提取出名为Authorization的内容，这是由前端决定的。
     * 实际上去看Vue代码能发现，前端在登陆成功后将后端传递的 jwtToken保存下来（怎么保存同样是前端的事情）
     *
     * @param serverHttpRequest 响应式Request
     * @return Request中携带的简易用户令牌
     */
    private String getJwtStringServer(ServerHttpRequest serverHttpRequest) {
        // 多了一个getFirst不知道是什么意思，是获取第一个 Authorization 嘛
        String jwtString = serverHttpRequest.getHeaders().getFirst(AuthConstant.JWT_AUTHENTICATION);
        if (!StringUtils.isEmpty(jwtString) && jwtString.startsWith(AuthConstant.JWT_PREFIX)) {
            jwtString = jwtString.replaceFirst(AuthConstant.JWT_PREFIX, "");
        }
        return jwtString;
    }

    /**
     * 获取缓存中 后端登录令牌 的 key。
     * 该方法与 ruoyi-common-security 中 TokenUtils中的同名方法完全相同。只是由于这个模块和ruoyi-common-security的交集实在太少，因此不依赖。
     *
     * @param userId    用户Id
     * @param userToken 登录令牌token
     * @return 加上前缀的key
     */
    private String getTokenCacheKey(String userId, String userToken) {
        return AuthConstant.LOGIN_USER_REDIS_PREFIX + userId + ":" + userToken;
    }


    /**
     * 内容编码
     *
     * @param str 内容
     * @return 编码后的内容
     */
    public static String urlEncode(String str) {
        try {
            return URLEncoder.encode(str, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            return "";
        }
    }

    /**
     * 内容解码
     *
     * @param str 内容
     * @return 解码后的内容
     */
    public static String urlDecode(String str) {
        try {
            return URLDecoder.decode(str, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            return "";
        }
    }
}
